﻿//-----------------------------------------------------------------------
// <copyright file="GDICanvas.cs" company="Nathan Miller">
// Copyright (c) Nathan Miller.
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
// 
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// </copyright>
//-----------------------------------------------------------------------
namespace TileTool.Base.Render
{
    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.Windows.Forms;
    using TileTool.Base.Render.Control;
    using TileTool.Common.Math;
    using TileTool.Input;
    using TileTool.Input.Focus;
    using TileTool.Input.Keyboard;
    using TileTool.Input.Mouse;
    using TileTool.Render;
    using TileTool.Render.Settings;
    using TileTool.Settings;

    /// <summary>
    /// Interface for a canvas that can be drawn into.
    /// </summary>
    public class GDICanvas : ICanvas
    {
        #region Fields - Private

        /// <summary>
        /// Control associated with this canvas.
        /// </summary>
        private GDICanvasControl control = null;

        /// <summary>
        /// Last mouse position we knew about.
        /// </summary>
        private Vector2D? mousePos = null;

        /// <summary>
        /// Current set of buttons that are pressed.
        /// </summary>
        private MouseButton buttons = MouseButton.None;

        /// <summary>
        /// Dictionary which tracks which keys are currently down.
        /// </summary>
        private Dictionary<Keys, bool> keyDownMap = new Dictionary<Keys, bool>();

        #endregion

        #region Constructor

        /// <summary>
        /// Canvas constructor.
        /// </summary>
        public GDICanvas()
        {
            GDICanvasControl newControl = new GDICanvasControl();

            newControl.BackColor = System.Drawing.SystemColors.Control;
            newControl.Dock = System.Windows.Forms.DockStyle.Fill;

            newControl.Resize += this.HandleControlResize;
            newControl.Paint += this.HandleControlPaint;
            newControl.KeyDown += this.HandleKeyDown;
            newControl.KeyUp += this.HandleKeyUp;
            newControl.MouseMove += this.HandleMouseMove;
            newControl.MouseDown += this.HandleMouseDown;
            newControl.MouseUp += this.HandleMouseUp;
            newControl.GotFocus += this.HandleGotFocus;
            newControl.LostFocus += this.HandleLostFocus;

            this.control = newControl;

            this.ResetKeyMap();
        }
        
        #endregion

        #region Events - Public

        /// <summary>
        /// Event triggered when the canvas is resized.
        /// </summary>
        public event Action<ICanvas> Resized;

        /// <summary>
        /// Event triggered when the canvas should be drawn.
        /// </summary>
        public event Action<ICanvas, ICanvasRenderer> Draw;

        /// <summary>
        /// Event triggered when there was input on the canvas.
        /// </summary>
        public event Action<ICanvas, InputData> Input;

        #endregion

        #region Properties - Public

        /// <summary>
        /// Gets the control associated with the canvas.
        /// </summary>
        public System.Windows.Forms.Control Control
        {
            get
            {
                return this.control;
            }
        }

        /// <summary>
        /// Gets the width of the canvas.
        /// </summary>
        public int Width
        {
            get
            {
                Debug.Assert(this.Control != null, "Control me be non-null.");
                return this.Control.Width;
            }
        }

        /// <summary>
        /// Gets the height of the canvas.
        /// </summary>
        public int Height
        {
            get
            {
                Debug.Assert(this.Control != null, "Control me be non-null.");
                return this.Control.Height;
            }
        }

        #endregion

        #region Methods - Public

        /// <summary>
        /// Create a camera and associate it with this canvas.
        /// </summary>
        /// <returns>Returns a new camera object.</returns>
        public ICamera CreateCamera()
        {
            return new GDICamera(this);
        }

        #endregion

        #region Methods - Private

        /// <summary>
        /// Handle the control paint event.
        /// </summary>
        /// <param name="sender">Event sender.</param>
        /// <param name="e">Paint event arguments.</param>
        private void HandleControlPaint(object sender, PaintEventArgs e)
        {
            Action<ICanvas, ICanvasRenderer> action = this.Draw;
            if (action != null)
            {
                GDICanvasRenderer renderer = this.control.Renderer;
                RenderSettings settings = SettingsSystem.Get<RenderSettings>();

                renderer.BeginRender(e.Graphics);
                renderer.Clear(settings.ClearColor);
                action(this, this.control.Renderer);
                renderer.EndRender();
            }
        }

        /// <summary>
        /// Handle the control being resized.
        /// </summary>
        /// <param name="sender">Event sender.</param>
        /// <param name="e">Resize arguments.</param>
        private void HandleControlResize(object sender, EventArgs e)
        {
            Action<ICanvas> resize = this.Resized;
            if (resize != null)
            {
                resize(this);
                this.Control.Invalidate();
            }
        }

        /// <summary>
        /// Handle a key down event.
        /// </summary>
        /// <param name="sender">Event sender.</param>
        /// <param name="e">Event data.</param>
        private void HandleKeyDown(object sender, KeyEventArgs e)
        {
            this.keyDownMap[e.KeyCode] = true;

            this.HandleKeyEvent(true, e);
        }

        /// <summary>
        /// Handle a key up event.
        /// </summary>
        /// <param name="sender">Event sender.</param>
        /// <param name="e">Event data.</param>
        private void HandleKeyUp(object sender, KeyEventArgs e)
        {
            this.keyDownMap[e.KeyCode] = false;

            this.HandleKeyEvent(false, e);
        }

        /// <summary>
        /// Handle a key event. Either up or down.
        /// 
        /// Don't update key map in here since it gets called from 
        /// functions while iterating over the key map.
        /// </summary>
        /// <param name="down">true if this is for a key down event.</param>
        /// <param name="e">Key event args.</param>
        private void HandleKeyEvent(bool down, KeyEventArgs e)
        {
            KeyInputData.Builder builder = new KeyInputData.Builder();

            builder.Down = down;
            builder.Alt = (e.Modifiers & Keys.Alt) != 0;
            builder.Control = (e.Modifiers & Keys.Control) != 0;
            builder.Shift = (e.Modifiers & Keys.Shift) != 0;
            builder.Key = (InputKey)e.KeyCode;

            this.TriggerInputEvent(builder.Build());
        }

        /// <summary>
        /// Handle a mouse move event.
        /// </summary>
        /// <param name="sender">Event sender.</param>
        /// <param name="e">Event data.</param>
        private void HandleMouseMove(object sender, MouseEventArgs e)
        {
            Vector2D newPos = new Vector2D((float)e.X, this.Height - (float)e.Y);
            MouseMoveInputData.Builder builder = new MouseMoveInputData.Builder();

            builder.Buttons = this.buttons;
            builder.LastPos = this.mousePos.HasValue ? this.mousePos.Value : newPos;
            builder.Pos = newPos;
            builder.Alt = this.keyDownMap[Keys.Menu];
            builder.Control = this.keyDownMap[Keys.ControlKey];
            builder.Shift = this.keyDownMap[Keys.ShiftKey];

            this.mousePos = newPos;

            this.TriggerInputEvent(builder.Build());
        }

        /// <summary>
        /// Handle a mouse down event.
        /// </summary>
        /// <param name="sender">Event sender.</param>
        /// <param name="e">Event data.</param>
        private void HandleMouseDown(object sender, MouseEventArgs e)
        {
            this.HandleMouseButtonEvent(true, e);
        }

        /// <summary>
        /// Handle a mouse down event.
        /// </summary>
        /// <param name="sender">Event sender.</param>
        /// <param name="e">Event data.</param>
        private void HandleMouseUp(object sender, MouseEventArgs e)
        {
            this.HandleMouseButtonEvent(false, e);
        }

        /// <summary>
        /// Handle a key event. Either up or down.
        /// </summary>
        /// <param name="down">true if this is for a key down event.</param>
        /// <param name="e">Mouse event args.</param>
        private void HandleMouseButtonEvent(bool down, MouseEventArgs e)
        {
            MouseButtonInputData.Builder builder = new MouseButtonInputData.Builder();
            MouseButton button = this.ConvertMouseButton(e.Button);

            if (down)
            {
                this.buttons |= button;
            }
            else
            {
                this.buttons &= ~button;
            }

            Vector2D pos = new Vector2D((float)e.X, this.Height - (float)e.Y);

            builder.Button = button;
            builder.Buttons = this.buttons;
            builder.Pos = pos;
            builder.Down = down;
            builder.Alt = this.keyDownMap[Keys.Menu];
            builder.Control = this.keyDownMap[Keys.ControlKey];
            builder.Shift = this.keyDownMap[Keys.ShiftKey];

            this.TriggerInputEvent(builder.Build());
        }

        /// <summary>
        /// Convert the passed Winforms mouse button to a generic button.
        /// </summary>
        /// <param name="button">Winforms mouse button to convert.</param>
        /// <returns>Returns a generic button.</returns>
        private MouseButton ConvertMouseButton(MouseButtons button)
        {
            switch (button)
            {
                case MouseButtons.Left:
                    return MouseButton.Left;
                case MouseButtons.Right:
                    return MouseButton.Right;
                case MouseButtons.Middle:
                    return MouseButton.Middle;
            }

            return MouseButton.None;
        }

        /// <summary>
        /// Convert the passed generic button to a Winforms button.
        /// </summary>
        /// <param name="button">Generic mouse button to convert.</param>
        /// <returns>Returns a generic button.</returns>
        private MouseButtons ConvertMouseButton(MouseButton button)
        {
            switch (button)
            {
                case MouseButton.Left:
                    return MouseButtons.Left;
                case MouseButton.Right:
                    return MouseButtons.Right;
                case MouseButton.Middle:
                    return MouseButtons.Middle;
            }

            return MouseButtons.None;
        }

        /// <summary>
        /// Handle gaining focus.
        /// </summary>
        /// <param name="sender">Event sender.</param>
        /// <param name="e">Event data.</param>
        private void HandleGotFocus(object sender, EventArgs e)
        {
            FocusInputData.Builder builder = new FocusInputData.Builder();

            builder.Focus = true;

            this.TriggerInputEvent(builder.Build());
        }

        /// <summary>
        /// Handle losing focus.
        /// </summary>
        /// <param name="sender">Event sender.</param>
        /// <param name="e">Event data.</param>
        private void HandleLostFocus(object sender, EventArgs e)
        {
            this.TriggerReleaseForAllDown();

            FocusInputData.Builder builder = new FocusInputData.Builder();

            builder.Focus = false;

            this.TriggerInputEvent(builder.Build());
        }

        /// <summary>
        /// Reset the key map by clearing it and then added in any default
        /// keys we want.
        /// </summary>
        private void ResetKeyMap()
        {
            this.keyDownMap.Clear();

            // Always set these so we can use the index operator without a check to see
            // if the key exists.
            this.keyDownMap[Keys.Menu] = false;
            this.keyDownMap[Keys.ControlKey] = false;
            this.keyDownMap[Keys.ShiftKey] = false;
        }

        /// <summary>
        /// Trigger the release event for all items that are currently down.
        /// </summary>
        private void TriggerReleaseForAllDown()
        {
            // Release all keys.
            foreach (KeyValuePair<Keys, bool> key in this.keyDownMap)
            {
                if (!key.Value)
                {
                    continue;
                }

                this.HandleKeyEvent(false, new KeyEventArgs(key.Key));
            }

            this.ResetKeyMap();

            // Release mouse buttons.
            Vector2D pos = this.mousePos.HasValue ? this.mousePos.Value : Vector2D.Zero;
            foreach (MouseButton button in Enum.GetValues(typeof(MouseButton)))
            {
                if ((this.buttons & button) == 0)
                {
                    continue;
                }

                MouseEventArgs args = new MouseEventArgs(this.ConvertMouseButton(button), 0, (int)pos.x, (int)pos.y, 0);

                this.HandleMouseButtonEvent(false, args);
            }
        }

        /// <summary>
        /// Trigger an input event for the passed input data.
        /// </summary>
        /// <param name="data">Input data for the event.</param>
        private void TriggerInputEvent(InputData data)
        {
            Action<ICanvas, InputData> action = this.Input;

            if (action != null)
            {
                action(this, data);
            }
        }

        #endregion
    }
}
